/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package uk.co.dupenois.grid;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.PathIterator;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JPanel;
import uk.co.dupenois.geometry.Coordinate;
import uk.co.dupenois.geometry.Line;

/**
 *
 * @author mpd209
 */
public class Grid extends JPanel implements MouseListener, MouseWheelListener, MouseMotionListener, ComponentListener{
    protected static Color COLOUR_GRID = Color.GRAY;
    protected static Color COLOUR_ORIGIN = Color.BLACK;
    protected static Color COLOUR_DOT = Color.RED;
    protected static Color COLOUR_CENTROID = Color.GREEN;
    protected static Color COLOUR_LINE = Color.BLUE;
    protected static Stroke STROKE_LINE = new BasicStroke(2f);

    private Coordinate[] coordinates;
    private ArrayList<BufferedImage> history;
    private int currentHistoryStep;
    private BufferedImage currentHistoryStepImage;

    private GraphicsConfiguration graphicsConfig;
    protected boolean showCoordinateLabels = false;
    private Line[] lines;

    private int gridSpacing;
    private GridListener listener;
    private Point bottomLeft;
    private boolean mouseIn;
    private Point dragStart;
    public double zoom;
    private Cursor normalCursor = new Cursor(Cursor.DEFAULT_CURSOR);
    private Cursor dragCursor = new Cursor(Cursor.MOVE_CURSOR);

    private Container container;

    private int maxHistoryLength = -1;
    private int historyCountTotal = 0;
    private File recordingFolder;

    private boolean showCentroid = false;

    public Grid(GridListener listener, Container container){
        this(listener, container, 5);
    }
    public Grid(GridListener listener, Container container, int gridSpacing){
        super();
        this.container = container;
        this.gridSpacing = gridSpacing;
        this.addMouseListener(this);
        this.addMouseMotionListener(this);
        this.addComponentListener(this);
        this.addMouseWheelListener(this);
        this.listener = listener;
        this.bottomLeft = new Point(-50, -50);
        this.zoom = 1;
        this.setDoubleBuffered(true);
        this.history = new ArrayList<BufferedImage>();
        GraphicsEnvironment ge =
                GraphicsEnvironment.getLocalGraphicsEnvironment();
        graphicsConfig = ge.getDefaultScreenDevice().getDefaultConfiguration();
    }

    public void showCentroid(){
        this.showCentroid = true;
    }

    public void hideCentroid(){
        this.showCentroid = false;
    }

    public void addLine(Line line){
        if(lines==null)lines = new Line[0];
        ArrayList<Line> linesList = new ArrayList<Line>();
        linesList.addAll(Arrays.asList(lines));
        linesList.add(line);
        this.lines = linesList.toArray(new Line[linesList.size()]);
        this.repaint();
    }

    public void removeLine(Line line){
        if(lines==null)lines = new Line[0];
        ArrayList<Line> linesList = new ArrayList<Line>();
        linesList.addAll(Arrays.asList(lines));
        linesList.remove(line);
        this.lines = linesList.toArray(new Line[linesList.size()]);
        this.repaint();
    }


 
    public void addCoordinate(Coordinate coordinate){
        if(coordinates==null)coordinates = new Coordinate[0];
        ArrayList<Coordinate> coords = new ArrayList<Coordinate>();
        coords.addAll(Arrays.asList(coordinates));
        coords.add(coordinate);
        this.coordinates = coords.toArray(new Coordinate[coords.size()]);
        this.repaint();
    }

    public void removeCoordinate(Coordinate coordinate){
        if(coordinates==null)coordinates = new Coordinate[0];
        ArrayList<Coordinate> coords = new ArrayList<Coordinate>();
        coords.addAll(Arrays.asList(coordinates));
        coords.remove(coordinate);
        this.coordinates = coords.toArray(new Coordinate[coords.size()]);
        this.repaint();
    }

    public void setRecordingFolder(String location){
        File tempFolder= new File(location);
        if(!tempFolder.isDirectory()){
            if(!tempFolder.mkdirs())
                throw new IllegalArgumentException("Cannot create folder: "+location);
        }
        recordingFolder = tempFolder;
    }
    public void record(){
        BufferedImage image = graphicsConfig.createCompatibleImage(this.getDimensions().width,
                this.getDimensions().height, BufferedImage.TRANSLUCENT);
        Graphics g = image.getGraphics();
        paint(g);
        if(recordingFolder!=null){
            String indexString = (historyCountTotal+"");
            int currentIndexLength = indexString.length();
            for(int i = currentIndexLength; i <= 5; i++) indexString = "0"+indexString;
            try {
                File imageFile = new File(recordingFolder.getCanonicalPath() + "/history[" + indexString + "].png");
                ImageIO.write(image, "png", imageFile);
            } catch (IOException ex) {
                System.out.println("Warning: failed to write image "+
                        recordingFolder.getPath() + "/history[" + indexString + "].png");
            }
        }
        if(maxHistoryLength>-1&&this.history.size()>=maxHistoryLength){
            //Shift down
            this.history.remove(0);
        }
        this.history.add(image);
        image.flush();
        historyCountTotal++;
    }

    public void setMaxHistoryLength(int length){
        this.maxHistoryLength = length;
    }

    public BufferedImage[] getHistory(){
        return this.history.toArray(new BufferedImage[this.history.size()]);
    }

    public boolean inPlayback(){
        return (this.currentHistoryStepImage!=null);
    }
    public int getCurrentPlayBackIndex(){
        if(inPlayback())return this.currentHistoryStep;
        return history.size()-1;
    }
    public boolean playback(int i){
        if(i>=0 && i< history.size()){
            this.currentHistoryStep = i;
            this.currentHistoryStepImage = history.get(i);
            this.repaint();
            return true;
        }else{
            return false;
        }
    }
    public void resumeNormalDisplay(){
        this.currentHistoryStepImage = null;
        this.currentHistoryStep = history.size()-1;
        this.repaint();
    }



    @Override
    public void paint(Graphics g){
        Graphics2D g2 = (Graphics2D)g;
        if(inPlayback()){
            g2.drawImage(this.currentHistoryStepImage, 0, 0, null);
            Font originalFont = g2.getFont();
            Stroke originalStroke = g2.getStroke();
            g2.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
            String playbackString = "Image: "+(this.currentHistoryStep+1)+"/"+this.history.size();
            int playbackStringWidth = g2.getFontMetrics().stringWidth(playbackString);
            int playbackStringHeight = g2.getFontMetrics().getHeight();
            g2.setStroke(new BasicStroke(2f));
            g2.setColor(Color.WHITE);
            g2.fillRect(10, 10, playbackStringWidth+16, playbackStringHeight+16);
            g2.setColor(Color.RED);
            g2.drawRect(10, 10, playbackStringWidth+16, playbackStringHeight+16);
            g2.setColor(Color.BLACK);
            g2.drawString(playbackString, 18, 18+(playbackStringHeight/2));
            g2.setFont(originalFont);
            g2.setStroke(originalStroke);
            return;
        }
        g2.setColor(Color.WHITE);
        g2.fillRect(0, 0, this.getDimensions().width, this.getDimensions().height);
        //Vertical Lines


        for(int i = bottomLeft.x; i < this.getBottomRight().x; i++){
//            if(debug)System.out.println("[i]: "+i);
            if(i!=0&&i%(gridSpacing*zoom)!=0)continue;
            if(i==0){
                g2.setColor(COLOUR_ORIGIN);
                g2.setStroke(new BasicStroke(3));
            }else{
                g2.setColor(COLOUR_GRID);
                g2.setStroke(new BasicStroke(1));
            }
            Point screenLineStart = getScreenPosition(new Point(i, bottomLeft.y));
                
            Point screenLineFinish = getScreenPosition(new Point(i, getTopLeft().y));

            g2.drawLine(screenLineStart.x, 0, screenLineFinish.x, this.getDimensions().height-1);
        }
        //Horizontal Lines
        for(int i = bottomLeft.y; i < this.getTopLeft().y; i++){
            if(i!=0&&i%(gridSpacing*zoom)!=0)continue;
            
            if(i==0){
                g2.setColor(COLOUR_ORIGIN);
                g2.setStroke(new BasicStroke(3));
            }else{
                g2.setColor(COLOUR_GRID);
                g2.setStroke(new BasicStroke(1));
            }
            Point screenLineStart = getScreenPosition(new Point(bottomLeft.x, i));

            
            Point screenLineFinish = getScreenPosition(new Point(getBottomRight().x, i));

            
            g2.drawLine(0, screenLineStart.y, this.getDimensions().width-1, screenLineFinish.y);

        }

        if(coordinates!=null){
            g2.setColor(COLOUR_DOT);
            Coordinate[] coordsClone = coordinates.clone();
            double x = 0;
            double y = 0;
            for(Coordinate d : coordsClone){
                x += d.getX();
                y += d.getY();
                Point screenLoc = getScreenPosition(d);
                g2.fillOval(screenLoc.x-5, screenLoc.y-5, 10, 10);
                if(showCoordinateLabels){
                    String str = d.toString(1);
                    int stringWidth = g2.getFontMetrics().stringWidth(str);
                    int stringHeight = g2.getFontMetrics().getHeight();
                    g2.drawString(str, screenLoc.x-(stringWidth/2), screenLoc.y-stringHeight);
                }
            }
            if(this.showCentroid){
                x = x/coordsClone.length;
                y = y/coordsClone.length;
                Point centroid = getScreenPosition(new Coordinate(x, y));
                g2.setColor(COLOUR_CENTROID);
                g2.fillOval(centroid.x-5, centroid.y-5, 10, 10);
            }
        }

        if(lines!=null){
            Stroke original = g2.getStroke();
            g2.setStroke(STROKE_LINE);
            g2.setColor(COLOUR_LINE);
            Line[] linesClone = lines.clone();
            Point screenLocStart;
            Point screenLocFinish;
            for(Line line : linesClone){
                screenLocStart = getScreenPosition(line.getStart());
                screenLocFinish = getScreenPosition(line.getFinish());
                g2.drawLine(screenLocStart.x, screenLocStart.y, screenLocFinish.x, screenLocFinish.y);
            }
            g2.setStroke(original);
        }
    }

    protected void drawShape(Color colour, Shape shape, Graphics2D g2){
        g2.setColor(colour);
        PathIterator pit = shape.getPathIterator(null);
        double[] seg = new double[6];
        Coordinate startLocation = null;
        Coordinate currentLocation = null;
        Coordinate previousLocation = null;
        while (!pit.isDone()) {
            int retVal = pit.currentSegment(seg);

            if(retVal == PathIterator.SEG_MOVETO) {
                currentLocation = new Coordinate(seg[0], seg[1]);
                startLocation = new Coordinate(seg[0], seg[1]);
            }else if (retVal == PathIterator.SEG_LINETO) {
                currentLocation = new Coordinate(seg[0], seg[1]);
                Point screenOrigin = getScreenPosition(previousLocation);
                Point screenFinish = getScreenPosition(currentLocation);
                g2.drawLine(screenOrigin.x, screenOrigin.y, screenFinish.x, screenFinish.y);
            }else { //SEG_CLOSE
                currentLocation = startLocation;
                Point screenOrigin = getScreenPosition(previousLocation);
                Point screenFinish = getScreenPosition(currentLocation);
                g2.drawLine(screenOrigin.x, screenOrigin.y,  screenFinish.x, screenFinish.y);
            }
            previousLocation = currentLocation;
            pit.next();
        }
    }

    public Point getBottomRight(){
        return new Point(bottomLeft.x+(int)Math.round(this.getDimensions().width*zoom), bottomLeft.y);
    }
    public Point getTopRight(){
        return new Point(bottomLeft.x+(int)Math.round(this.getDimensions().width*zoom), bottomLeft.y+(int)Math.round(this.getDimensions().height*zoom));

    }
    public Point getTopLeft(){
        return new Point(bottomLeft.x, bottomLeft.y+(int)Math.round(this.getDimensions().height*zoom));

    }
    public Point getScreenPosition(Coordinate coord){
        Point point = coordinateToPoint(coord);
        return getScreenPosition(point);
    }
    public Point getScreenPosition(Point point){
        return new Point(
                ((int)Math.round(point.x/zoom))-((int)Math.round(this.bottomLeft.x/zoom)),
                this.getDimensions().height-(((int)Math.round(point.y/zoom))-((int)Math.round(this.bottomLeft.y/zoom)))
                );

    }
    public Point getRealFromScreen(Point point){
        return new Point(
                (this.bottomLeft.x+(int)Math.round(point.x*zoom)),
                (int)Math.round((this.getDimensions().height-point.y)*zoom)+this.bottomLeft.y
                );

    }
    public static Point coordinateToPoint(Coordinate coord){
        int x = (int)Math.round(coord.getX());
        int y = (int)Math.round(coord.getY());
        return new Point(x, y);
    }
    private void zoomBy(double amount){
        if(this.zoom>=1&&this.zoom+amount < 1){
            this.zoom = 1+(amount*0.1);
        }else if (this.zoom < 1) {
            this.zoom += (amount*0.1);
        }else{
            this.zoom += amount;
        }
        if(this.zoom < 0.2){
            this.zoom = 0.2;
        }
        this.repaint();
    }
    public void mouseClicked(MouseEvent arg0) {
    }

    public void mousePressed(MouseEvent arg0) {
        dragStart = new Point(arg0.getX(), arg0.getY());
        if(mouseIn)  setCursor(dragCursor);
    }

    public void mouseReleased(MouseEvent arg0) {
        setCursor(normalCursor);
    }

    public void mouseEntered(MouseEvent arg0) {
        mouseIn = true;

    }

    public void mouseExited(MouseEvent arg0) {
        mouseIn = false;
        setCursor(normalCursor);
    }
    private void moveTo(int x, int y){
        bottomLeft.move(x, y);
    }
    private void moveBy(int xdiff, int ydiff){
        bottomLeft.move(bottomLeft.x-xdiff, bottomLeft.y+ydiff);
    }

    public void mouseDragged(MouseEvent arg0) {
        if(mouseIn){
            setCursor(dragCursor);
            int xdiff = (int)Math.round((arg0.getX() - dragStart.x)*zoom);
            int ydiff = (int)Math.round((arg0.getY() - dragStart.y)*zoom);
            dragStart = new Point(arg0.getX(), arg0.getY());
            moveBy(xdiff, ydiff);
            this.repaint();
        }
    }

    public void mouseMoved(MouseEvent arg0) {

        Point real = getRealFromScreen(new Point(arg0.getX(), arg0.getY()));

        this.listener.mouseLocationMoved(real.x, real.y);
    }

    /**
     * @return the dimensions
     */
    public Dimension getDimensions() {
        return this.container.getSize();
    }


    public void componentResized(ComponentEvent arg0) {
        this.repaint();
    }

    public void componentMoved(ComponentEvent arg0) {
    }

    public void componentShown(ComponentEvent arg0) {
    }

    public void componentHidden(ComponentEvent arg0) {
    }

    public void mouseWheelMoved(MouseWheelEvent e) {
        zoomBy((double)e.getWheelRotation());
        Point real = getRealFromScreen(new Point(e.getX(), e.getY()));
        this.listener.mouseLocationMoved(real.x, real.y);
    }

    /**
     * @return the zoom
     */
    public double getZoom() {
        return zoom;
    }
   

}
